// Model.cpp
#include <math.h>
#include "Model.h"
#include <fstream>
#include <sstream>
#include <cstdlib>//include pour l'exit(code_erreur)
using namespace std;

// Les membres static non constants doient �tres initialisées dans le .cpp
Model *Model::instance = 0;

/*
 * Constructeur du model chargé d'initialisé tout les attributs de celui-ci
 */
Model::Model() {
	score = 0;
	nombreGhost = 0;
	scoreToReach = 0;
	currentLevel = 1;
	/*concaténer une string avec un entier*/
	std::stringstream ssLevel;
	string texte = "Level: ";
	ssLevel << texte << currentLevel;
	msgToShow = ssLevel.str(); //Level : 1
	/*                                    */
	endMsg = "";
	gameEnded = false;
	lightRed = true;
	pushUp = false, pushDown = false, pushRight = false, pushLeft = false;
	zoomFactor = 1;
	modeFirstPerson = false;
	selectionMode = false;
	viewRotation = 0;
	rotation = false;
	allowFirstPersonView = false;
	drawAnimation = false;
	cadence = TEMPSBASE2D;
}

/*
 * Fonction chargée de modifier la position du pacman, impliquant de se fait
 * la gestion du remplacement d'une pastille par une case vide, etc ...
 */
void Model::modifierPositionPacMan(int deplacementY, int deplacementX) {
	myPacMan.setIdle(false); //le pacman est en mouvement
	myLabyrinthe->setElement(myPacMan.getCurrentPosY(),
			myPacMan.getCurrentPosX(), 'E'); //ancien emplacement devient vide(empty)
	myPacMan.setCurrentPosX(myPacMan.getCurrentPosX() + deplacementX); //enregistre la nouvelle position en X
	myPacMan.setCurrentPosY(myPacMan.getCurrentPosY() + deplacementY); //enregistre la nouvelle position en Y
	myLabyrinthe->setElement(myPacMan.getCurrentPosY(),
			myPacMan.getCurrentPosX(), 'M'); //nouvel emplacement devient le PacMan
}

/*
 * Fonction chargée de modifier la position d'un ghost(numéro du ghost passé en paramètre)
 */
void Model::modifierPositionGhost(int nextY, int nextX, int ghostNumber) {
	myLabyrinthe->setElement(ghost_tab[ghostNumber].getCurrentPosY(),
			ghost_tab[ghostNumber].getCurrentPosX(),
			ghost_tab[ghostNumber].getElemBefore()); //remet en place l'élément se trouvant avant le ghost
	this->ghost_tab[ghostNumber].setElemBefore(
			myLabyrinthe->getElement(nextY, nextX)); //enregistre le nouvel élément à retenir de la nouvelle case où se trouve le ghost
	myLabyrinthe->setElement(nextY, nextX, 'G'); //position le ghost à ce nouvel emplacement
	this->ghost_tab[ghostNumber].setCurrentPosY(nextY); //enregistre la nouvelle position en Y
	this->ghost_tab[ghostNumber].setCurrentPosX(nextX); //enregistre la nouvelle position en X
}

/*
 * Fonction chargée de faire changer de coté le pacman lorsque celui atteint une case 'L' -> Limite horizontale
 */
void Model::switchSides() {
	myLabyrinthe->setElement(myPacMan.getCurrentPosY(),
			myPacMan.getCurrentPosX(), 'E'); //ancienne emplacement devient vide(empty)
	if (myPacMan.getCurrentPosX() == 1) { //limite gauche
		myPacMan.setCurrentPosX(myLabyrinthe->getMatrixWidth() - 2); //on le place à droite
	} else { //limite droite
		myPacMan.setCurrentPosX(1); //on le place à gauche
	}
	myLabyrinthe->setElement(myPacMan.getCurrentPosY(),
			myPacMan.getCurrentPosX(), 'M'); //nouvel emplacement devient le PacMan
}

/*
 * Fonction permettant d'incrémenter le score de 'inc' point(s)
 */
void Model::incScore(int inc) {
	score += inc;
}

/*
 * Fonction permettant de définir si une partie est terminé ou pas(gagné ou perdu)
 */
void Model::setGameEnded(bool isGameEnded) {
	this->gameEnded = isGameEnded;
}

/*
 * Fonction permettant de définir le message que l'on affiche à l'utilisateur(level : 1, etc..)
 */
void Model::setMsgToShow(string msgToShow) {
	this->msgToShow = msgToShow;
}

/*
 * Fonction permettant de définir à quel niveau l'utilisateur se trouve
 */
void Model::setCurrentLevel(int currentLevel) {
	this->currentLevel = currentLevel;
}

/*
 * Fonction permettant de définir combien de point(s) l'utilisateur doit obtenir pour passer au niveau suivant
 * (en pratique ce sera toutes les pastilles)
 */
void Model::setScoreToReach(int scoreToReach) {
	this->scoreToReach = scoreToReach;
}

/*
 * Pattern Singleton
 */
Model *Model::getInstance() {
	if (instance == 0) {
		instance = new Model();
	}
	return instance;
}

/*
 * Fonction chargée d'ajouter un nouveau ghost avec sa position X et Y dans le table de ghost
 */
void Model::addGhost(int posY, int posX) {
	Ghost newGhost(posY, posX);
	this->ghost_tab[nombreGhost] = newGhost;
	nombreGhost++;
}

/*
 * Fonction demandant à tous les ghosts de rafaîchir leurs directions actuelles en fonction de leurs déplacements en X et Y
 * Cela est nécessaire car il faut savoir la direction des 'Mouvable' afin d'implémenter des mouvements en continu
 */
void Model::refreshGhostsDirection() {
	for (int i = 0; i < nombreGhost; i++) {
		this->ghost_tab[i].defineDirection();
	}
}

/*
 * Fonction permettant de mettre un ghost en 'prison', en le faisant renaître au 'Birth Place' des ghosts
 */
void Model::resetGhost(int ghostNumber) {
	myLabyrinthe->setElement(this->ghost_tab[ghostNumber].getCurrentPosY(),
			this->ghost_tab[ghostNumber].getCurrentPosX(),
			this->ghost_tab[ghostNumber].getElemBefore()); //remet en place l'élément se trouvant avant le ghost
	this->ghost_tab[ghostNumber].setElemBefore('E'); //en prison, il n'y a que des cases vides donc le ghost remplace un 'E'
	this->ghost_tab[ghostNumber].setCurrentPosY(
			myLabyrinthe->getBPLaceGhostY()); //modifie position du ghost à la position du BP(birth place) pour Y
	this->ghost_tab[ghostNumber].setCurrentPosX(
			myLabyrinthe->getBPLaceGhostX()); //modifie position du ghost à la position du BP(birth place) pour X
	myLabyrinthe->setElement(myLabyrinthe->getBPLaceGhostY(),
			myLabyrinthe->getBPLaceGhostX(), 'G'); //modifie la matrice pour y mettre le ghost à sa nouvelle position
}

/*
 * Fonction permettant de remettre les ghost et le pacman à leurs places initiales lorsque le pacman perd une vie
 */
void Model::resetGame() {
	myLabyrinthe->setElement(myPacMan.getCurrentPosY(),
			myPacMan.getCurrentPosX(), 'E'); //remplace la position acutelle du pacman par une case vide
	myLabyrinthe->setElement(myPacMan.getInitialY(), myPacMan.getInitialX(),
			'M'); //remet le pacman à sa position initiale
	myPacMan.setCurrentPosY(myPacMan.getInitialY()); //enregistre nouvelle position en Y
	myPacMan.setCurrentPosX(myPacMan.getInitialX()); //enregistre nouvelle position en X

	/*
	 * même chose pour chaque ghosts
	 */
	for (int i = 0; i < nombreGhost; i++) {
		myLabyrinthe->setElement(ghost_tab[i].getCurrentPosY(),
				ghost_tab[i].getCurrentPosX(), ghost_tab[i].getElemBefore());
		ghost_tab[i].setElemBefore('E');
		myLabyrinthe->setElement(ghost_tab[i].getInitialY(),
				ghost_tab[i].getInitialX(), 'G');
		ghost_tab[i].setCurrentPosY(ghost_tab[i].getInitialY());
		ghost_tab[i].setCurrentPosX(ghost_tab[i].getInitialX());
	}
}

/*
 * Fonction permettant de redemarrer une partie en réinitialisant tous les attributs correctement
 * et supprimant les espaces mémoires nécessaires afin d'éviter un memory leak
 */
void Model::restartGame() {
	score = 0;
	nombreGhost = 0;
	scoreToReach = 0; //reinitialise le tableau de ghost
	zoomFactor = 1;
	viewRotation = 0;
	rotation = false;
	currentLevel = 1;
	modeFirstPerson = false;
	drawAnimation = false;
	delete myLabyrinthe;
	myPacMan.setVieRestantes(LIFEPACMAN);
	myPacMan.setModeSuper(false);
	this->gameEnded = false;
	ifstream myConfig("GameSettings1.conf"); //recharge le niveau 1 grâce à la surcharge de l'opérateur
	if (myConfig.is_open()) {
		myConfig >> *this;
		myConfig.close();
	} else {
		cout << "Unable to open file";
		std::exit(1);
	}
	std::stringstream ssmsg;
	string texte = "Level: ";
	ssmsg << texte << currentLevel;
	string msg = ssmsg.str();
	this->setMsgToShow(msg);
}

/*
 * Fonction permettant de changer de niveau en initialisant tous les attributs correctement
 * et supprimant les espaces mémoires nécessaires afin d'éviter un memory leak
 */
void Model::switchLevel() {
	score = 0;
	nombreGhost = 0;
	myPacMan.setModeSuper(false);
	scoreToReach = 0;
	delete myLabyrinthe;
	std::stringstream ssLevel;
	string texte = "GameSettings";
	ssLevel << texte << (++currentLevel) << ".conf";
	string nextLevel = ssLevel.str();
	ifstream myConfig(nextLevel.c_str());
	if (myConfig.is_open()) {
		myConfig >> *this;
		myConfig.close();
	} else {
		cout << "Unable to open file";
		std::exit(1);
	}
	std::stringstream ssmsg;
	texte = "Level: ";
	ssmsg << texte << currentLevel;
	string msg = ssmsg.str();
	this->setMsgToShow(msg);
}

/*
 * Fonction permettant de définir un message à l'utilisateur lorsque la partie est terminée
 */
void Model::setEndMsg(string endMsg) {
	this->endMsg = endMsg;
}

/*Renvoie le zoom à effectuer */
float Model::getZoomFactor() const {
	return zoomFactor;
}

/* Renvoie 'vrai' si on se trouve en mode première personne */
bool Model::isModeFirstPerson() const {
	return modeFirstPerson;
}

/* Renvoie si on est en mode séléction*/
bool Model::isSelectionMode() const {
	return selectionMode;
}
/* Renvoie l'angle de rotation */
float Model::getViewRotation() const {
	return viewRotation;
}

/* Renvoie 'vrai' si le labyrinthe est entrain de tourner */
bool Model::isRotation() const {
	return rotation;
}

/*
 * Méthode gérant les mouvements du pacman lorsque et effectue les changements de déplacements
 * et de direction selon les choix de l'utilisateur.
 */
void Model::setPacmanDirection(int newDirection) {
	switch (newDirection) {
	case LEFTDIRECTION:
		if (myPacMan.getCurrentDirection() == LEFTDIRECTION) { //ne rien faire si déjà dans cette direction
			return;
		}
		if (myPacMan.getCurrentDirection() == RIGHTDIRECTION) {
			myPacMan.turnBack(); //demi tour si on allait vers la droite
		} else if (myPacMan.getCurrentDirection() == UPDIRECTION) {
			myPacMan.turnLeft(); //tourne à gauche si on allait vers le haut

			/*else if car il se pourrait que la direction soit NONDIRECTION(initial par exemple),
			 * auquel cas, il ne fait pas faire de rotation.
			 */
		} else if (myPacMan.getCurrentDirection() == DOWNDIRECTION) {
			myPacMan.turnRight(); //tourne à droite si on allait vers le bas
		}
		myPacMan.setLeftDirection();
		break;

	case RIGHTDIRECTION:
		if (myPacMan.getCurrentDirection() == RIGHTDIRECTION) { //ne rien faire si déjà dans cette direction
			return;
		}
		if (myPacMan.getCurrentDirection() == LEFTDIRECTION
				|| myPacMan.getCurrentDirection() == NONEDIRECTION) {
			myPacMan.turnBack(); //demi tour si on position initial ou qu'on allait sur la gauche
		} else if (myPacMan.getCurrentDirection() == UPDIRECTION) {
			myPacMan.turnRight(); //tourne à droite si on allait vers le haut
		} else {
			myPacMan.turnLeft(); //tourne à gauche si on allait vers le bas
		}
		myPacMan.setRightDirection();
		break;

	case UPDIRECTION:
		if (myPacMan.getCurrentDirection() == UPDIRECTION) { //ne rien faire si déjà dans cette direction
			return;
		}
		if (myPacMan.getCurrentDirection() == LEFTDIRECTION
				|| myPacMan.getCurrentDirection() == NONEDIRECTION) {
			myPacMan.turnRight(); //tourne à droite si on allait à gauche ou position intiale
		} else if (myPacMan.getCurrentDirection() == RIGHTDIRECTION) {
			myPacMan.turnLeft(); //tourne à gauche si on allait à droite
		} else {
			myPacMan.turnBack(); //demi si on allait vers le bas
		}
		myPacMan.setUpDirection();
		break;

	case DOWNDIRECTION:
		if (myPacMan.getCurrentDirection() == DOWNDIRECTION) { //ne rien faire si déjà dans cette direction
			return;
		}
		if (myPacMan.getCurrentDirection() == LEFTDIRECTION
				|| myPacMan.getCurrentDirection() == NONEDIRECTION) {
			myPacMan.turnLeft(); //tourne à gauche si on allait à gauche ou position initiale
		} else if (myPacMan.getCurrentDirection() == RIGHTDIRECTION) {
			myPacMan.turnRight(); //tourne à droite si on allait déjà vers la droite
		} else {
			myPacMan.turnBack(); //demi tour si on allait vers le haut
		}
		myPacMan.setDownDirection();
		break;
	default:
		break;
	}
}

/*
 * Méthode gérant les mouvements du pacman lorsque et effectue les changements de déplacements
 * et de direction selon les choix de l'utilisateur.
 * Cette méthode à la particularité de gérer les mouvements avec un vue à la première personne.
 */
void Model::setPacmanDirectionFirstPerson(int newDirection) {
	switch (newDirection) {
	case LEFTDIRECTION:
		myPacMan.turnLeft();

		if (myPacMan.getCurrentDirection() == LEFTDIRECTION) {
			myPacMan.setDownDirection();
		} else if (myPacMan.getCurrentDirection() == RIGHTDIRECTION) {
			myPacMan.setUpDirection();
		} else if (myPacMan.getCurrentDirection() == UPDIRECTION) {
			myPacMan.setLeftDirection();
		} else { //down
			myPacMan.setRightDirection();
		}
		break;

	case RIGHTDIRECTION:
		myPacMan.turnRight();

		if (myPacMan.getCurrentDirection() == LEFTDIRECTION) {
			myPacMan.setUpDirection();
		} else if (myPacMan.getCurrentDirection() == RIGHTDIRECTION) {
			myPacMan.setDownDirection();
		} else if (myPacMan.getCurrentDirection() == UPDIRECTION) {
			myPacMan.setRightDirection();
		} else { //down
			myPacMan.setLeftDirection();
		}
		break;

	case DOWNDIRECTION:
		myPacMan.turnBack();

		if (myPacMan.getCurrentDirection() == LEFTDIRECTION) {
			myPacMan.setRightDirection();
		} else if (myPacMan.getCurrentDirection() == RIGHTDIRECTION) {
			myPacMan.setLeftDirection();
		} else if (myPacMan.getCurrentDirection() == UPDIRECTION) {
			myPacMan.setDownDirection();
		} else { //down
			myPacMan.setUpDirection();
		}
		break;

	case UPDIRECTION:
		//ne rien faire sauf si aucune direction n'a encore été initialisée(début de jeu)
		if (myPacMan.getCurrentDirection() == NONEDIRECTION) {
			myPacMan.setLeftDirection();
		}
		break;

	default:
		break;
	}
}
/* Renvoie 'vrai' si on a le droit d'entrer en vue première personne */
bool Model::isAllowFirstPersonView() const {
	return allowFirstPersonView;
}

/* Permet de savoir si la vue doit dessiner l'animation du bras pacman */
bool Model::isDrawAnimation() const {
	return drawAnimation;
}

/* Renvoie le pacman */
PacMan Model::getPacMan() const {
	return myPacMan;
}

/* Pour permettre ou pas l'animation du bras du pacman */
void Model::setDrawAnimation(bool drawAnimation) {
	this->drawAnimation = drawAnimation;
}

/* Permet d'interdire ou pas la vue première personne */
void Model::setAllowFirstPersonView(bool allowFirstPersonView) {
	this->allowFirstPersonView = allowFirstPersonView;
}

/* Permet de faire tourner le labyrinthe */
void Model::setRotation(bool isRotation) {
	this->rotation = isRotation;
}

/*
 * définit l'angle avec lequel effectuer une rotation sur un axe vertical central du plan de jeu
 */
void Model::setViewRotation(float viewRotation) {
	this->viewRotation = viewRotation;
	if (this->viewRotation > 360) { // Pour éviter les overflow
		this->viewRotation = this->viewRotation - 360;
	}
}

/*
 * Permet de définir si nous nous trouvons en mode de séléction ou de rendering.
 * Utile pour le gluPickMatrix afin d'implémenter une zone central clickable sur le plateau de jeu
 */
void Model::setSelectionMode(bool isSelectionMode) {
	this->selectionMode = isSelectionMode;
}

/* Permet de définir si nous nous trouvons dans la vue première personne  */
void Model::setModeFirstPerson(bool modeFirstPerson) {
	this->modeFirstPerson = modeFirstPerson;
}

/*
 * Set le zoom à effectuer,
 */
void Model::setZoomFactor(float zoomFactor) {
	this->zoomFactor = zoomFactor;
}

/*
 * Fonction permettant de modifier l'animation des ghosts
 */
void Model::setLightRed(bool value) {
	this->lightRed = value;
}

/*
 * Surchage de l'operateur >> permettant de chargé un niveau dans la classe labyrinthe
 */
istream& operator>>(istream& myConfig, Model& model) {
	string line;
	string temp;
	int cmp;
	string token;
	string key_value[2];
	int tmpWidth;
	int tmpHeight;
	while (getline(myConfig, line)) { //boucle variable de configuration
		if (line.length() != 0) {
			stringstream lineStream(line);
			cmp = 0;
			while (lineStream >> token) { //sépare les mots espacés d'un blanc(comportement par défaut)
				key_value[cmp++] = token; //0->key 1->value
			}
			if (key_value[0].compare("width") == 0) {
				//assignation variable et conversion d'un type string au type entier
				if (!(stringstream(key_value[1]) >> tmpWidth)) {
					cout << "Problème de conversion pour le token: "
							<< key_value[1];
					std::exit(1);
				}
			} else if (key_value[0].compare("height") == 0) {

				//assignation variable et conversion d'un type string au type entier
				if (!(stringstream(key_value[1]) >> tmpHeight)) {
					cout << "Problème de conversion pour le token: "
							<< key_value[1];
					std::exit(1);
				}
			}
		} else { //ligne vide délimite les variables et le labyrinthe
			break;
		}
	}

	//partie matrice

	model.myLabyrinthe = new Labyrinthe(tmpHeight, tmpWidth); //construction en mémoire de la matrice vide
	unsigned row = 0;

	while (getline(myConfig, line)) { //boucle labyrinthe
		for (unsigned column = 0; column < line.length(); column++) {
			if (line[column] == 'M') { //M == pacman // enregistrement de sa position intiale et donc actuelle
				model.myPacMan.setCurrentPosY(row);
				model.myPacMan.setCurrentPosX(column);
				model.myPacMan.setInitialY(row);
				model.myPacMan.setInitialX(column);
			}
			if (line[column] == 'P') { //P == pastille
				model.scoreToReach += VAL_P; //incrémente le score à atteindre en fonction de la valeur d'une pastille
			}
			if (line[column] == 'S') { //S == SUPER pastille
				model.scoreToReach += VAL_S; //incrémente le score à atteindre en fonction de la valeur d'une super pastille
			}
			if (line[column] == 'G') { //G == ghost
				model.addGhost(row, column); //ajout de nouveau ghost dans le tableau des ghosts
			}
			if (line[column] == 'B') { //B == Birth place, l'endroit où les ghosts renaissancent une fois manger par le Super Pacman
				model.myLabyrinthe->registerGhostBirthPLace(row, column); //enregistrement de la position du BP
			}
			char element = line[column];
			model.myLabyrinthe->setElement(row, column, element); //ajout des éléments dans la matrice
		}
		row++;
	}
	return myConfig;
}

/* Permet de définir si le pacman est en mode super ou pas*/
void Model::setPacmanSuperMode(bool value) {
	myPacMan.setModeSuper(value);
}

/* Appel la méthode pour animer le pacman*/
void Model::animeBouchePacman() {
	myPacMan.animBouche();
}

/* Permet de définir si le pacman ne doit rien faire ou pas*/
void Model::setPacmanIdle(bool value) {
	myPacMan.setIdle(value);
}

/* Permet de définir le nombre de vie restantes pour le pacman */
void Model::setPacmanVieRestantes(int vieRestantes) {
	myPacMan.setVieRestantes(vieRestantes);
}

/* Permet de définir si un ghost peut quitter la prison */
void Model::setGhostCanLeaveJail(int position, bool value) {
	this->ghost_tab[position].setCanLeaveJail(value);
}

/* Permet de définir le déplacement du ghost en X */
void Model::setGhostDeplacementX(int position, int deplacement) {
	this->ghost_tab[position].setDeplacementX(deplacement);
}

/* S'occupe de gérer correctement la fin d'une partie */
void Model::handleEndGame(string msg) {
	this->gameEnded = true; //signale  que le jeu est fini(perdu ou gagné)
	this->setEndMsg(msg); //message informant l'utilisateur qu'il a perdu/gagné
	modeFirstPerson = false; //permettra de positionner la camera correctement pour afficher le message
	this->zoomFactor = 1; //permettra de positionner la camera correctement pour afficher le message
}

/* Permet de définir le déplacement du ghost en Y */
void Model::setGhostDeplacementY(int position, int deplacement) {
	this->ghost_tab[position].setDeplacementY(deplacement);
}

